package com.google.code.p.restrpc.web;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.google.code.p.restrpc.DefaultRpcResponse;
import com.google.code.p.restrpc.RequestData;
import com.google.code.p.restrpc.RpcFault;
import com.google.code.p.restrpc.RpcRequest;
import com.google.code.p.restrpc.core.RpcServiceException;
import com.google.code.p.restrpc.core.RpcServiceNotAllowedException;
import com.google.code.p.restrpc.core.RpcServiceNotFoundException;
import com.google.code.p.restrpc.core.RpcServiceProxy;
import com.google.code.p.restrpc.core.RpcServiceRepository;
import com.google.code.p.restrpc.core.async.AsynServiceRequest;
import com.google.code.p.restrpc.core.async.AsyncRequestIdNotFoundException;
import com.google.code.p.restrpc.core.async.AsyncServiceManager;
import com.google.code.p.restrpc.core.url.AsyncHeader;

/**
 * @author dz
 */
public class DefaultRpcServiceDispatcher implements
                                        RpcServiceDispatcher,
                                        UrlContextAware {
    
    private static final Log logger = LogFactory.getLog(DefaultRpcServiceDispatcher.class);
    
    private static Set<String> notAllowed = new HashSet<String>();
    
    static {
        notAllowed.addAll(Arrays.asList(new String[] { "getClass",
                                                      "hashCode",
                                                      "equals",
                                                      "toString",
                                                      "clone",
                                                      "notify",
                                                      "notifyAll",
                                                      "wait",
                                                      "finalize" }));
    }
    
    private String urlContext;
    
    private AsyncServiceManager asyncServiceManager;
    
    /**
     * @param urlContext
     */
    public DefaultRpcServiceDispatcher() {
        super();
    }
    
    /**
     * @param contextUrl
     */
    public DefaultRpcServiceDispatcher(String contextUrl) {
        super();
        this.urlContext = contextUrl;
    }
    
    /**
     * @return the asyncServiceManager
     */
    public AsyncServiceManager getAsyncServiceManager() {
        return asyncServiceManager;
    }
    
    /**
     * @param asyncServiceManager
     *            the asyncServiceManager to set
     */
    public void setAsyncServiceManager(AsyncServiceManager asyncServiceManager) {
        this.asyncServiceManager = asyncServiceManager;
    }
    
    /**
     * @return the contextUrl
     */
    public String getUrlContext() {
        return urlContext;
    }
    
    /**
     * @param contextUrl
     *            the contextUrl to set
     */
    public void setUrlContext(String contextUrl) {
        this.urlContext = contextUrl;
    }
    
    /**
     * 
     */
    public void dispatch(HttpServletRequest httpRequest,
                         HttpServletResponse httpResponse) throws IOException,
                                                          ServletException {
        try {
            String method = httpRequest.getMethod();
            
            String uri = httpRequest.getRequestURI()
                                    .substring(httpRequest.getContextPath()
                                                          .length());
            String requestPart = uri.substring(urlContext.length());
            if (StringUtils.isEmpty(requestPart)) {
                logger.warn("Invalid URL(empty) for RestRpc Request!");
                throw new ServletException("Invalid URL(empty) for RestRpc Request!");
            }
            String serviceName = resolveServiceName(requestPart);
            if (StringUtils.isEmpty(serviceName)) {
                logger.warn("Invalid URL(service name is not specified) for RestRpc Request!");
                throw new ServletException("Invalid URL(service name is not specified) for RestRpc Request!");
            }
            String methodName = resolveMethodName(requestPart);
            if (StringUtils.isEmpty(methodName)) {
                logger.warn("Invalid URL(operation name is not specified) for RestRpc Request!");
                throw new ServletException("Invalid URL(operation name is not specified) for RestRpc Request!");
            }
            
            if (!RpcServiceRepository.getInstance().isRegistered(serviceName)) {
                if (logger.isWarnEnabled()) {
                    logger.warn("The RpcService[name=" + serviceName
                                + "] not found!");
                }
                throw new ServletException("The RpcService[name=" + serviceName
                                           + "] not found!");
            }
            
            RequestData requestData = parseRequestData(httpRequest);
            if (RequestData.isAsync(requestData)) {
                String requestId = RequestData.getRequestId(requestData);
                if (StringUtils.isEmpty(requestId)) {
                    throw new AsyncRequestIdNotFoundException("The async request id is not found for RpcService[name=" + serviceName
                                                              + ",method="
                                                              + methodName
                                                              + "]!");
                }
                
                AsynServiceRequest request = AsynServiceRequest.create(requestId,
                                                                       serviceName,
                                                                       methodName,
                                                                       requestData.getBody());
                
                asyncServiceManager.getAsyncServiceQueue()
                                   .addAsynServiceRequest(request);
                
                httpResponse.setHeader("CONTENT-TYPE", "text/json");
                httpResponse.getOutputStream()
                            .print(JSON.toJSONString(DefaultRpcResponse.SUCCEED_RESPONSE,
                                                     SerializerFeature.WriteClassName));
                httpResponse.getOutputStream().flush();
            }
            else {
                JSONArray jsonArray = (JSONArray) requestData.getBody();
                // JSONArray jsonArray = parseJSONArray(httpRequest);
                
                RpcServiceProxy rpcServiceProxy = RpcServiceRepository.getInstance()
                                                                      .getServiceProxy(serviceName);
                
                Object result = (jsonArray == null) ? rpcServiceProxy.invoke(methodName)
                                                   : rpcServiceProxy.invoke(methodName,
                                                                            jsonArray);
                
                DefaultRpcResponse rpcResponse = new DefaultRpcResponse();
                rpcResponse.setSucceed(true);
                rpcResponse.setResult(result);
                
                httpResponse.setHeader("CONTENT-TYPE", "text/json");
                httpResponse.getOutputStream()
                            .print(JSON.toJSONString(rpcResponse,
                                                     SerializerFeature.WriteClassName));
                httpResponse.getOutputStream().flush();
            }
            
        }
        catch (RpcServiceNotFoundException e) {
            logger.error(e, e);
            httpResponse.setStatus(404);
        }
        catch (RpcServiceNotAllowedException e) {
            logger.error(e, e);
            httpResponse.setStatus(403);
        }
        catch (Throwable e) {
            logger.error(e, e);
            
            DefaultRpcResponse rpcResponse = new DefaultRpcResponse();
            rpcResponse.setSucceed(false);
            RpcFault fault = new RpcFault();
            if (e.getCause() != null) {
                fault.setMessage(e.getMessage() + " Caused : "
                                 + e.getCause().getMessage());
            }
            else {
                fault.setMessage(e.getMessage());
            }
            rpcResponse.setFault(fault);
            
            httpResponse.setHeader("CONTENT-TYPE", "text/json");
            httpResponse.getOutputStream()
                        .print(JSON.toJSONString(rpcResponse,
                                                 SerializerFeature.WriteClassName));
            httpResponse.getOutputStream().flush();
        }
    }
    
    private static final RequestData dummyRequestData = new RequestData();
    
    private RequestData parseRequestData(HttpServletRequest httpRequest) throws IOException,
                                                                        ServletException {
        RequestData result = doParseRequestData(getRequestedJsonString(httpRequest));
        if (result == null) {
            return dummyRequestData;
        }
        
        Map head = result.getHead();
        if (head != null) {
            Object asyncValue = head.get(RpcRequest.ASYNC_KEY);
            if (asyncValue != null) {
                if (asyncValue instanceof JSONObject) {
                    JSONObject jsonObject = (JSONObject) asyncValue;
                    AsyncHeader asyncHeader = JSON.toJavaObject(jsonObject,
                                                                AsyncHeader.class);
                    head.put(RpcRequest.ASYNC_KEY, asyncHeader);
                }
                else {
                    head.put(RpcRequest.ASYNC_KEY, asyncValue);
                }
            }
        }
        return result;
    }
    
    private JSONArray parseJSONArray(HttpServletRequest httpRequest) throws IOException,
                                                                    ServletException {
        return doParseJSONArray(getRequestedJsonString(httpRequest));
    }
    
    private String getRequestedJsonString(HttpServletRequest httpRequest) throws IOException,
                                                                         ServletException {
        String method = httpRequest.getMethod();
        String bodyStr = null;
        if ("GET".equalsIgnoreCase(method)) {
            bodyStr = httpRequest.getParameter("BODY");
        }
        else if ("POST".equalsIgnoreCase(method)) {
            BufferedReader br = new BufferedReader(new InputStreamReader(httpRequest.getInputStream()));
            StringBuilder stringBuffer = new StringBuilder();
            String str = "";
            while ((str = br.readLine()) != null) {
                stringBuffer.append(str);
            }
            bodyStr = stringBuffer.toString();
        }
        else {
            throw new ServletException("Unsupported http method " + method);
        }
        
        return bodyStr;
    }
    
    private RequestData doParseRequestData(String jsonStr) {
        if (StringUtils.isEmpty(jsonStr)) {
            return null;
        }
        
        return JSON.parseObject(jsonStr, RequestData.class);
    }
    
    private JSONArray doParseJSONArray(String jsonStr) {
        if (StringUtils.isEmpty(jsonStr)) {
            return null;
        }
        
        if ((jsonStr.indexOf("\"[") == 0) && (jsonStr.lastIndexOf("]\"") == (jsonStr.length() - 2))) {
            String text = jsonStr.substring(1, jsonStr.length() - 1);
            return JSON.parseArray(text);
        }
        
        return JSON.parseArray(jsonStr);
    }
    
    private JSONObject parseJSONObject(HttpServletRequest httpRequest) throws IOException,
                                                                      ServletException {
        String method = httpRequest.getMethod();
        String bodyStr = null;
        if ("GET".equalsIgnoreCase(method)) {
            bodyStr = httpRequest.getParameter("BODY");
        }
        else if ("POST".equalsIgnoreCase(method)) {
            BufferedReader br = new BufferedReader(new InputStreamReader(httpRequest.getInputStream()));
            StringBuilder stringBuffer = new StringBuilder();
            String str = "";
            while ((str = br.readLine()) != null) {
                stringBuffer.append(str);
            }
            
            if (StringUtils.isEmpty(stringBuffer.toString().trim())) {
                return null;
            }
            
            if ((stringBuffer.indexOf("\"{") == 0) && (stringBuffer.lastIndexOf("}\"") == (stringBuffer.length() - 2))) {
                return JSON.parseObject(stringBuffer.substring(1,
                                                               stringBuffer.length() - 1));
            }
            
            bodyStr = stringBuffer.toString();
        }
        else {
            throw new ServletException("Unsupported http method " + method);
        }
        
        return JSON.parseObject(bodyStr);
    }
    
    private String resolveServiceName(String uri) throws RpcServiceException {
        int end = uri.indexOf('/', 1);
        if (end < 0) {
            throw new RpcServiceException("Invalid URL for RestRpc Request!");
        }
        String result = uri.substring(1, end);
        return result.startsWith("/") ? result.substring(1) : result;
    }
    
    private String resolveMethodName(String uri) throws RpcServiceException {
        int start = uri.indexOf('/', 1);
        if (start < 0) {
            throw new RpcServiceException("Invalid URL for RestRpc Request!");
        }
        int startBack = uri.lastIndexOf('/');
        if (startBack != start) {
            throw new RpcServiceException("Invalid URL for RestRpc Request!");
        }
        
        String result = uri.substring(start);
        return result.startsWith("/") ? result.substring(1) : result;
    }
    
}
